import random
import operator
import time
import sys


def play_poker(nplayers, ncards, nrounds):
    if nrounds/10 == 0: print_rounds = False
    else: print_rounds = True
    start = time.clock()
    start_deck = [r+s for r in '23456789TJQKA' for s in 'SHDC']
    win_count, winning_hand, hand_count, play_poker.draw_count, unique_wranks, total_wranks = {}, {}, {}, {1:0, 2:0, 3:0, 4:0}, {}, {}
    play_poker.drawn_cards = {}
    for round in range(nrounds):
        if print_rounds:
            if round % (nrounds/10) == 0: print 'Running round {0}'.format(round)
        round_rank = {}
        hands, remaining = deal(nplayers, ncards, start_deck)
        for hand in hands:
            for card in hand:
                if card in play_poker.drawn_cards:
                    play_poker.drawn_cards[card] += 1
                else: play_poker.drawn_cards[card] = 1
            current_hand = hand_rank(hand)
            p_hand = current_hand[-1]

            """ Rank hand and improve if possible. """
            if current_hand[0] > 3: round_rank[hands.index(hand)] = (current_hand[0], current_hand[-1]) # can't be improved, return the result
            elif current_hand[0] > 0: round_rank[hands.index(hand)], remaining = do_kinds(current_hand[3], current_hand[4], ncards, remaining) # at least one pair, improve and return result
            elif current_hand[2][0] > 2: round_rank[hands.index(hand)], remaining = do_flush(hand, current_hand[2], ncards, remaining) # possible flush, improve and return result
            else: round_rank[hands.index(hand)], remaining = do_straight(current_hand[3], ncards, remaining) # try a straight if possible, if not draw from high card
        winner = max(round_rank.items(), key=operator.itemgetter(1))[0]
        if winner in win_count:
            win_count[winner] += 1
        else:
            win_count[winner] = 1
        winning_hand, unique_wranks, total_wranks, hand_count = build_stats(winner,round_rank[winner],winning_hand,unique_wranks,total_wranks,hand_count,round_rank)
    stop = time.clock()
    print 'This run with {0} rounds lasted {1} seconds.\nAverage time per round was {2} seconds.'.format(nrounds, stop, stop/nrounds)
    return win_count, winning_hand, hand_count, play_poker.draw_count

def allmax(iterable, key=None):
    "Return a list of all items equal to the max of the iterable."
    result, maxval = [], None
    key = key or (lambda x: x)
    for x in iterable:
        xval = key(x)
        if not result or xval > maxval:
            result, maxval = [x], maxval
        elif xval == maxval:
            result.append(x)
    return result

def build_stats(winner, rank, winning_hand, unique_wranks, total_wranks, hand_count, round_rank):
    # First stat: number of unique appearances of a rank in winning hands
    # Second stat: number of total appearances of a rank in winning hands
    # Third stat: winning_hand
    if rank[0] in winning_hand:
        winning_hand[rank[0]] += 1
    else:
        winning_hand[rank[0]] = 1
    # Fourth stat: number of wins for a hand divided by number of appearances
    for hand in set(a[0] for a in round_rank.values()):
        if hand in hand_count:
            hand_count[hand] += 1
        else:
            hand_count[hand] = 1
    return winning_hand, unique_wranks, total_wranks, hand_count

def do_kinds(rankset, count, ncards, remaining):
    """ Accepts the set of ranks, the count, number of cards in hand and the remaining deck. """
    k_hand = []
    for r in range(len(rankset)):
        if count[r] > 1: k_hand.extend([rankset[r]] * count[r])
    p_hand, remaining = draw(k_hand, ncards, remaining)
    p_hand = hand_rank(p_hand)
    p_hand =(p_hand[0], p_hand[3], p_hand[-1])
    return p_hand, remaining

def do_flush(hand, s_count, ncards, remaining):
    """ Accepts a list of ranks, a tuple comprised of the max count of suits and the suit, number of cards in hand and the remaining deck. """
    if s_count[0] > 2: p_hand = [card for card in hand if s_count[1] in card]
    p_hand, remaining = draw(p_hand, ncards, remaining)
    r_hand = hand_rank(p_hand)
    if r_hand[0] == 5:
        p_hand = (r_hand[0], r_hand[1], r_hand[-1])
    else:
        p_hand = (r_hand[0], r_hand[3], r_hand[-1])
    return p_hand, remaining

def do_straight(hand, ncards, remaining):
    """ Accepts a list of ranks, number of cards per hand and remaining deck. """
    test_straight = []
    hand = list(hand)
    if 14 in hand and len([s for s in hand if s < 6]) > 2: hand[hand.index(14)] = 1
    high, low, high2, low2 = hand[1:], hand[:-1], hand[2:], hand[:-2]
    if max(high) - min(high) < 5: hand = high
    elif max(low) - min(low) < 5: hand = low
    elif max(high2) - min(high2) < 5: hand = high2
    elif max(low2) - min(low2) < 5: hand = low2
    if len(hand) == 5: hand = [max(hand)]
    hand, remaining = draw(hand, ncards, remaining)
    if 14 in hand and 1 in hand: hand[hand.index(1)] = 14
    hand = tuple(hand)
    p_hand = hand_rank(hand)
    p_hand =(p_hand[0], p_hand[3], p_hand[-1])
    return p_hand, remaining


def deal(numhands, n, deck):
    random.shuffle(deck)
    return [deck[n*i:n*(i + 1)] for i in range(numhands)], deck[n*(i+1):]

def draw(p_hand, ncards, remaining):
    drawn = []
    for draws in range(ncards - len(p_hand)):
        drawn.append(remaining.pop(0))
    try:
        play_poker.draw_count[len(drawn)] += 1
    except KeyError:
        print len(drawn), 'is not in the draw_count dictionary.'
    for card in drawn:
        if card in play_poker.drawn_cards:
            play_poker.drawn_cards[card] += 1
        else: play_poker.drawn_cards[card] = 1
    if isinstance(p_hand[0], basestring) == False: # if the input hand contains ranks, ie anything not a flush, reduce the drawn list to ranks.
        drawn = ['--23456789TJQKA'.index(r) for r, s in drawn]
    p_hand = sorted(list(p_hand) + drawn, reverse = True)
    return p_hand, remaining

def hand_rank(hand, suits = []):
    if isinstance(hand[0], basestring) == True:
        if len(hand[0]) == 2: # if the first element of the input list is a string with length 2, pull out ranks and suits
            ranks = sorted([('--23456789TJQKA'.index(r), s) for r, s in hand], reverse = True)
            hand, suits = zip(*ranks)
            s_count = max([(suits.count(s), s) for s in suits])
        else:
            s_count = max([(hand.count(s), s) for s in hand])
            suits = list(set(hand))
    else:
        s_count = None
    if hand == (14, 5, 4, 3, 2): hand = (5, 4, 3, 2, 1) # if there is an ace-low straight, make the ace a 1
    ranks_v = sorted([(hand.count(s), s) for s in set(hand)], reverse = True)
    ranks_c, ranks_v = zip(*ranks_v) # split the ranks into a count of ranks sorted by most common and a list of ranks
    return (8 if (1, 1, 1, 1, 1) == ranks_c and max(hand) - min(hand) == 4 and len(set(suits)) == 1 else # straight flush
            7 if (4, 1) == ranks_c else # 4 kind
            6 if (3, 2) == ranks_c else # full house
            5 if len(set(suits)) == 1 else # flush
            4 if (1, 1, 1, 1, 1) == ranks_c and max(hand) - min(hand) == 4 else # straight
            3 if (3, 1, 1) == ranks_c else # 3 kind
            2 if (2, 2, 1) == ranks_c else # 2 pair
            1 if (2, 1, 1, 1) == ranks_c else # 2 kind
            0, suits, s_count, ranks_v, ranks_c, hand)


def run_it(nplayers, ncards, rounds):
    print 'Running poker simulation with {0} players, {1}-card hands and {2} total rounds.'.format(nplayers, ncards, rounds)
    win_count, winning_hand, hand_count, draw_count = play_poker(nplayers, ncards, rounds)
    while True:
        print ''
        print ''
        print 'What would you like to see?\n1.  Win stats\n2.  Hand stats\n3.  Nothing'
        print ''
        try:
            choice = raw_input('>')
        except KeyboardInterrupt:
            print 'Quitting on bad input.'
            break
        else:
            if choice == '1': win_stats(nplayers, rounds, win_count, winning_hand, hand_count)
            elif choice == '2': hand_stats(nplayers, rounds, draw_count)
            else: break


def win_stats(nplayers, rounds, win_count, winning_hand, hand_count):
    hand_values = {0: 'HC',
                   1: '1P',
                   2: '2P',
                   3: '3K',
                   4: 'ST',
                   5: 'FL',
                   6: 'FH',
                   7: '4K',
                   8: 'SF'}
    print ''
    print ''
    print '*** Showing some winning statistics ***'
    print ''
    print 'The count for winning players:'
    for player, count in win_count.items():
        print 'Player', '{0:2}: {1}'.format(player + 1, count)
    print ''
    print 'Distribution of winning hands:'
    for hand, count in winning_hand.items():
        print '{0:2}: {1:6} ==> {2:9}'.format(hand_values[hand], winning_hand[hand], (100.0 * winning_hand[hand] / rounds))
    print ''
    print 'Distribution of all hands:'
    print 'Hand ==> # showing ==> % of hands ==> % of rounds'
    for hand, count in hand_count.items():
        print '{0:2}: {1:6}   ==>   '.format(hand_values[hand], hand_count[hand]), '{0:.5f}   ==>'.format(100.0 * hand_count[hand] / sum(hand_count.values())), '{0:.3f}'.format(100.0 * hand_count[hand] / rounds)
    print ''
    print 'Percentage of hands as winners:'
    for hand, count in winning_hand.items():
        print '{0:2}: {1:.5f}'.format(hand_values[hand], (100.0 * count / hand_count[hand]))
    return


def hand_stats(nplayers, rounds, draw_count):
    d_count = sorted(play_poker.drawn_cards.items(), key = operator.itemgetter(1), reverse = True)
    print ''
    print ''
    print '*** Showing some statistics on cards ***'
    print ''
    print 'How many cards were drawn for each hand?'
    for drawn, count in draw_count.items():
        print '{0} cards drawn: {1}'.format(drawn, count)
    print ''
    print 'Most popular cards in hand:\n', '\n'.join('%s: %d' % (drawn, count) for drawn, count in d_count[:5])
    print ''
    print 'Least popular cards in hand:\n', '\n'.join('%s: %d' % (drawn, count) for drawn, count in d_count[-5:])
    print ''
    return

def main():
    tries = 0
    args = sys.argv[1:]
    if not args:
        print ''
        print ''
        while True:
            try:
                players = raw_input('Number of players? ')
                rounds = raw_input('Number of rounds? ')
            except KeyboardInterrupt:
                if tries > 1: print 'Quitting with bad input.'; sys.exit()
                else:
                    print 'Bad input, please enter a number of players (1-5) and a number of rounds.'
                    print ''
                    tries += 1
            else:
                break
        run_it(int(players), 5, int(rounds))
    else:
        run_it(int(args[0]), int(args[1]), int(args[2]))

if __name__ == '__main__':
  main()
